package pers.mor.tomcat.http;

import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.URLUtil;
import cn.hutool.log.LogFactory;
import pers.mor.tomcat.catalina.*;

import javax.servlet.RequestDispatcher;
import javax.servlet.ServletContext;
import javax.servlet.http.Cookie;
import javax.servlet.http.HttpSession;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.nio.charset.StandardCharsets;
import java.util.*;

public class Request extends BaseRequest {

    private String requestString;
    private String uri;
    private String port;
    private Socket socket;
    private Context context;
    private Host host;
    private String method;
    private String queryString;
    private Map<String,String[]> paramMap;
    private Map<String, String> headerMap;
    private Cookie[] cookies;
    private HttpSession session;

    private boolean forwarded = false;
    private boolean include =false;
    private Map<String, Object> attributesMap;

    public Request(Socket socket) throws IOException {
        this.socket = socket;
        this.paramMap = new HashMap<>();
        this.headerMap = new HashMap<>();
        this.attributesMap = new HashMap<>();
        this.parseHttpRequest();
        if (StrUtil.isEmpty(this.requestString)) {
            return;
        }
        this.parseHeaders();
        this.parseCookies();
        this.parseMethod();
        this.parseParameters();
        this.parseUri();
        this.parseContext();
        if (this.context == null) {
            String msg = StrUtil.format("访问的服务不存在 {}:{}{}", this.host.getName(), this.port, this.uri);
            LogFactory.get().warn(msg);
            return;
        }
        if (!context.getPath().equals("/")) {
            this.uri = StrUtil.removePrefix(this.uri, context.getPath());
            if (this.uri.equals("/") || this.uri.equals("")) {
                this.uri = "/index.html";
            }
        }
    }

    private void parseHttpRequest() throws IOException {
        InputStream in = this.socket.getInputStream();
        byte[] bytes = this.readBytes(in);
        this.requestString = new String(bytes, StandardCharsets.UTF_8);
        if (this.requestString == null || this.requestString.trim().equals("")) {
            this.requestString = "";
            return;
        }
        String hostPortStr = StrUtil.subBetween(this.requestString, "Host:", "\r\n").trim();
        this.port = StrUtil.subAfter(hostPortStr, ":", true);
        Service service = Server.getService(this.port);
        Engine engine = service.getEngine();
        String hostString = StrUtil.subBefore(hostPortStr, ":", true);
        if (hostString.equals(Constant.localhost)) {
            hostString = engine.getDefaultHost();
        }
        this.host = engine.getHostMap().get(hostString);
    }

    private void parseHeaders() {

        String requestString = this.requestString;
        requestString = StrUtil.subBefore(requestString, "\r\n\r\n", false);
        String reqStr = URLUtil.decode(requestString);

        String[] reqStrLines = reqStr.split("\r\n");
        for (int i = 1; i < reqStrLines.length; i++) {
            String[] strings = reqStrLines[i].split(":");
            String headName = strings[0].toLowerCase();
            String headValue = strings[1];
            this.headerMap.put(headName, headValue);
        }
    }

    private void parseCookies() {
        String cookiesStr = this.headerMap.get("cookie");
        if (cookiesStr == null || cookiesStr.equals("")) {
            this.cookies = new Cookie[0];
            return;
        }
        String[] nameValueArray = cookiesStr.split("; ");
        this.cookies = new Cookie[nameValueArray.length];
        List<Cookie> list = new ArrayList<>();
        for (String nameValue : nameValueArray) {
            String[] nv = nameValue.split("=");
            String name = nv[0];
            String value = nv[1];
            Cookie cookie = new Cookie(name.trim(), value);
            list.add(cookie);
        }
        list.toArray(this.cookies);
    }

    private String getJSessionIdFromCookie() {
        if (this.cookies == null || this.cookies.length == 0) {
            return null;
        }
        for (Cookie cookie : this.cookies) {
            if (cookie.getName().equals("JSESSIONID")) {
                return cookie.getValue();
            }
        }
        return null;
    }

    private void parseMethod() {
        this.method = StrUtil.subBefore(this.requestString, " ", false).trim();
    }

    private void parseParameters() {
        String method = this.method.toUpperCase();
        if (method.equals("GET")) {
            String temp = StrUtil.subBetween(this.requestString, " ", " ");
            this.queryString = StrUtil.subAfter(temp, "?", false);
        }
        if (this.method.equals("POST")) {
            this.queryString = StrUtil.subAfter(this.requestString, "\r\n\r\n", false);
        }
        if (this.queryString == null || this.queryString.equals("")) {
            return;
        }
        this.queryString = URLUtil.decode(this.queryString);
        String[] nameValues = this.queryString.split("&");
        for (String nameValue : nameValues) {
            if (nameValue.trim().equals("")) {
                continue;
            }
            String[] nv = nameValue.split("=");
            String name = nv[0];
            String value = nv[1];
            String[] values = this.paramMap.get(name);
            if (values == null) {
                values = new String[]{value};
                this.paramMap.put(name, values);
            } else {
                String[] tempValues = new String[values.length + 1];
                for (int i = 0; i < values.length; i++) {
                    tempValues[i] = values[i];
                }
                tempValues[values.length] = value;
                this.paramMap.put(name, values);
            }
        }
    }

    private void parseUri() {
        String temp = StrUtil.subBetween(this.requestString, " ", " ");
        if (!StrUtil.contains(temp,"?")) {
            this.uri = temp;
            return;
        }
        this.uri = StrUtil.subBefore(temp, "?", false);
    }

    private void parseContext() {
        String path = StrUtil.subBetween(this.uri, "/", "/");
        if (path == null) {
            path = this.uri;
        } else {
            path = "/" + path;
        }
        this.context = this.host.getContextMap().get(path);
        if (context == null) {
            context = this.host.getContextMap().get("/");
        }
    }

    public byte[] readBytes(InputStream in) throws IOException {
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        int bufferSize = 1048576;
        byte[] buffer = new byte[bufferSize];
        while(true) {

            int len = in.read(buffer);

            if (len > 0 && len < bufferSize) {
                byte[] b = new byte[len];
                for (int i = 0; i < len; i++) {
                    b[i] = buffer[i];
                }
                buffer = b;
            } else if(len <= 0) {
                return new byte[0];
            }

            baos.write(buffer, 0, len);

            if(len != bufferSize){
                break;
            }
        }
        return baos.toByteArray();
    }

    public Socket getSocket() {
        return this.socket;
    }

    public String getRequestString() {
        return requestString;
    }

    public void setUri(String uri) {
        this.uri = uri;
    }

    public String getUri() {
        return uri;
    }

    public Context getContext() {
        return context;
    }

    public void setContext(Context context) {
        this.context = context;
    }

    void setSession(HttpSession session) {
        this.session = session;
    }

    public boolean isForwarded() {
        return this.forwarded;
    }

    public void setForwarded(boolean forwarded) {
        this.forwarded = forwarded;
    }

    public boolean isInclude() {
        return this.include;
    }

    public void setInclude(boolean include) {
        this.include = include;
    }

    public RequestDispatcher getRequestDispatcher(String uri) {
        return new ApplicationRequestDispatcher(uri);
    }

    @Override
    public Object getAttribute(String s) {
        return this.attributesMap.get(s);
    }

    @Override
    public Enumeration<String> getAttributeNames() {
        return Collections.enumeration(this.attributesMap.keySet());
    }

    @Override
    public void setAttribute(String s, Object o) {
        this.attributesMap.put(s, o);
    }

    @Override
    public void removeAttribute(String s) {
        this.attributesMap.remove(s);
    }

    @Override
    public String getRequestedSessionId() {
        return this.getJSessionIdFromCookie();
    }

    @Override
    public HttpSession getSession(boolean b) {
        if (b == true) {
            return this.getSession();
        } else {
            String sessionId = this.getRequestedSessionId();
            return SessionManager.takeOutSessionFromMap(sessionId);
        }
    }

    @Override
    public HttpSession getSession() {
        return this.session;
    }

    @Override
    public Cookie[] getCookies() {
        return this.cookies;
    }

    @Override
    public String getMethod() {
        return method;
    }

    @Override
    public String getParameter(String s) {
        String[] strings = this.paramMap.get(s);
        if (strings != null && strings.length != 0) {
            return strings[0];
        }
        return null;
    }

    @Override
    public Enumeration<String> getParameterNames() {
        return Collections.enumeration(this.paramMap.keySet());
    }

    @Override
    public String[] getParameterValues(String s) {
        return this.paramMap.get(s);
    }

    @Override
    public Map<String, String[]> getParameterMap() {
        return this.paramMap;
    }

    @Override
    public String getHeader(String s) {
        return this.headerMap.get(s.toLowerCase());
    }

    @Override
    public Enumeration<String> getHeaderNames() {
        return Collections.enumeration(this.headerMap.keySet());
    }

    @Override
    public int getIntHeader(String s) {
        String i = this.headerMap.get(s);
        return Convert.toInt(i,0);
    }

    @Override
    public int getServerPort() {
        Integer port = Integer.valueOf(this.port);
        return port;
    }

    @Override
    public ServletContext getServletContext() {
        return this.context.getServletContext();
    }

    @Override
    public String getRealPath(String path) {
        return this.getServletContext().getRealPath(path);
    }

    @Override
    public String getLocalName() {
        String hostName = this.socket.getLocalAddress().getHostName();
        return hostName;
    }

    @Override
    public String getLocalAddr() {
        String hostAddress = this.socket.getLocalAddress().getHostAddress();
        return hostAddress;
    }

    @Override
    public int getLocalPort() {
        int localPort = this.socket.getLocalPort();
        return localPort;
    }

    @Override
    public String getProtocol() {
        return "HTTP:/1.1";
    }

    @Override
    public String getRemoteAddr() {
        InetSocketAddress socketAddress = (InetSocketAddress)this.socket.getRemoteSocketAddress();
        String temp = socketAddress.getAddress().toString();
        String remoteAddr = StrUtil.subAfter(temp, "/", false);
        return remoteAddr;
    }

    @Override
    public String getRemoteHost() {
        InetSocketAddress socketAddress = (InetSocketAddress)this.socket.getRemoteSocketAddress();
        String hostName = socketAddress.getHostName();
        return hostName;
    }

    @Override
    public int getRemotePort() {
        return this.socket.getPort();
    }

    @Override
    public String getScheme() {
        return "http";
    }

    @Override
    public String getServerName() {
        return this.getHeader("host").trim();
    }

    @Override
    public String getContextPath() {
        String path = this.context.getPath();
        if (path.equals("/")) {
            return "";
        }
        return path;
    }

    @Override
    public String getRequestURI() {
        return this.uri;
    }

    @Override
    public StringBuffer getRequestURL() {
        StringBuffer url = new StringBuffer();
        String http = this.getScheme();
        url.append(http);
        url.append("://");
        String serverName = this.getServerName();
        url.append(serverName);
        Integer port = Integer.valueOf(this.port);

        // --------- 以下两个if判断是从Tomcat8.5源码中抄过来的 --------
        if (port < 0) {
            port = 80; // Work around java.net.URL bug
        }
        if ((http.equals("http") && (port != 80))
                || (http.equals("https") && (port != 443))) {
            url.append(':');
            url.append(port);
        }
        // ---------------------------------------------------------

        url.append(this.getContextPath());
        url.append(this.uri);
        return url;
    }

    @Override
    public String getServletPath() {
        return this.uri;
    }
}
